Zum Hauptinhalt springen

Einführung in die Softwarearchitekturen

Angestrebtes Ziel einer Architektur ist die Gliederung des Systems in Einheiten mit explizit festgelegten Aufgaben und Abhängigkeiten zwischen diesen Einheiten.

Die Erfassung dieser Abhängigkeiten und Aufgaben erfolgt über die Schnittstellen. Hiermit werden die Einheiten so gekapselt, dass eine verbindliche Regelung besteht, in welcher Form von außen über die Schnittstelle auf eine gekapselte Einheit eingewirkt werden kann.

(1) Entwurfsprinzipien

Beim Entwurf und Implementierung von Softwarearchitekturen hält man sich an grundsätzlichen Entwurfsprinzipien. Diese stellen dabei einen Leitfaden dar, um die unvermeidliche Komplexität moderner Softwaresysteme beherrschbar zu machen und eine unkontrollierte Verflechtung der Komponenten zu verhindern. Erst durch diese methodische Ordnung wird sichergestellt, dass das System über seinen gesamten Lebenszyklus hinweg wartbar, testbar und flexibel gegenüber neuen Anforderungen bleibt.

(1.1) Kapselung und Information Hiding

Kapselung bedeutet, dass der Zugriff auf ein Software-Modul nicht direkt erfolgt, sondern kontrolliert über eine definierte Schnittstelle. Das Innere eines Moduls ist geschützt. Auf interne Daten oder Zustände (die Implementierung) kann von außen nicht direkt zugegriffen werden. Die Schnittstelle definiert genau, was in das Modul hineingeht und was herauskommt.

Information Hiding

Ein zentraler Aspekt der Kapselung ist das Information Hiding (Geheimnisprinzip). Der Nutzer des Moduls muss nur die Schnittstelle kennen (das „Was“). Er muss nicht wissen, wie das Modul intern funktioniert. Der Entwickler des Moduls muss sicherstellen, dass die Schnittstelle bedient wird (das „Wie“). Er kann die interne Umsetzung ändern, solange die Schnittstelle gleich bleibt.

Vorteile

  • Klare Architektur: Systeme sind besser strukturiert.
  • Wiederverwendbarkeit: Module können leichter in anderen Kontexten genutzt werden.
  • Sicherheit: Entwickler können sich auf die Einhaltung der Anforderungen verlassen.

Nachteile

  • Höherer Aufwand: Das Design und die Planung der Schnittstellen kosten anfangs mehr Zeit.
  • Performance: Die Abstraktion und das Laden passender Implementierungen können (geringe) Leistungseinbußen verursachen.

(1.2) KISS - so komplex wie nötig, so einfach wie möglich

Die Abkürzung KISS (von engl.: Keep it smart and simple) bezeichnet ein Entwurfsprinzip, nach welchem zu einem gegebenen Problem eine einfach gehaltene, leicht verständliche und dennoch tragfähige Lösung entwickelt werden soll. Ziel der angestrebten Vereinfachung ist die Fehler- und Wartungsminimierung.

(1.3) Kopplung und Kohäsion

Kopplung (Coupling) und Kohäsion (Cohesion) bestimmen, wie gut eine Software wartbar, erweiterbar und stabil ist.

In der Softwarearchitektur strebt man fast immer eine lose Kopplung (Low Coupling) und eine hohe Kohäsion (High Cohesion). Die Module (Komponenten/Klassen) sollen untereinander möglichst wenig voneinander wissen (lose Kopplung), aber innerlich sehr fokussiert auf genau eine Aufgabe sein (hohe Kohäsion).

Kopplung (Beziehung zwischen den Modulen)

Kopplung beschreibt, wie stark zwei Module voneinander abhängig sind. Wenn Module stark gekoppelt sind, führt eine Änderung in Modul A oft zu einem Fehler im Modul B (Dominoeffekt). Das Ziel ist es, so wenig Abhängigkeiten zwischen den Modulen zu haben. Ein Modul sollte eigenständig funktionieren.

Je nach Abhängigkeitsgrad, lassen sich unterschiedliche Arten von Kopplung feststellen:

  • Pathologische / Inhaltskopplung (Sehr schlecht): Ein Modul greift direkt auf das Innere eines anderen Moduls ein (verletzt die Kapselung).
  • Bereichskopplung (Common Environment): Zwei Module teilen sich globale Daten (z. B. globale Variablen). Das ist fehleranfällig.
  • Kontrollkopplung: Ein Modul steuert den Ablauf eines anderen (z. B. durch "Flags" oder Steuerparameter). Das aufrufende Modul weiß zu viel darüber, wie das andere arbeitet.
  • Datenkopplung (Gut): Module tauschen nur reine Daten (Parameter) aus. Sie wissen nichts voneinander, nur was rein und raus geht.

Kohäsion (Zusammenhalt innerhalb eines Moduls)

Kohäsion beschreibt, wie gut die Teile innerhalb eines einzigen Moduls logisch zusammengehören. Jedes Modul (oder jede Klasse/Komponente) soll genau eine klar definierte Aufgabe haben (Single-Responsibility-Principle). Eine Klasse "Rechnungsdrucker" sollte beispielsweise nur Rechnungen drucken und nicht gleichzeitig noch E-Mails versenden oder die Datenbank bereinigen. Der Vorteil liegt bei einem Fehler darin, dass man gezielt suchen kann.

(1.3) Separation of Concerns

Separation of Concerns (SoC) heißt grundsätzlich, dass verschiedene Aufgaben nicht vermischt werden sollen. Software wird komplex, wenn man Fachlichkeit (was die Software tut) und Technik (wie sie es speichert/überträgt) vermischt. Aspekte wie Sicherheit sind oft „Querschnittsaufgaben“ (Cross-cutting concerns), die man isoliert betrachten und lösen sollte, anstatt sie überall in den Code zu streuen.

Die „Quasar-Blutgruppen“

Um das Prinzip des SoC in der Praxis zu veranschaulichen, entwickelte Siedersleben im Projekt Quasar ein Klassifizierungssystem (Die Quasar Blutgruppen). Jede Komponente oder Klasse im System bekommt eine „Blutgruppe“, die ihre Rolle definiert. Ziel ist es, technische und fachliche Teile sauber zu trennen.

  • 0 (Basis / Unabhängig): Grundlegende Bausteine ohne Fachlichkeit oder spezielle Technik (z. B. Standard-Bibliotheken, Strings, Integers). Wiederverwendbarkeit: Sehr hoch (überall nutzbar)
  • T (Technik): Technische Schnittstellen oder Middleware (z. B. Datenbank-Treiber, APIs). Sie sind unabhängig vom Fachinhalt, hängen aber an einer Plattform. Wiederverwendbarkeit: Hoch (auf gleicher Plattform)
  • A (Anwendung (App)): Die reine Fachlogik (z. B. Klasse „Kunde“, „Konto“). Enthält keine technische Infrastruktur. Wiederverwendbarkeit: Gering (nur in dieser Domäne)
  • R: Repräsentation „Übersetzer“. Wandelt Fachobjekte in Formate für die Außenwelt um (z. B. XML, JSON, Verschlüsselung). Wiederverwendbarkeit: Nicht vorhanden (aber generierbar)
  • AT (Anwendung + Technik): Hier ist Fachlogik fest mit Technik verdrahtet. Das führt zu „Legacy-Code“ (Altlasten), der schwer zu warten ist. Wiederverwendbarkeit: Sehr schlecht (zu vermeiden)

Software-Metriken sind Messinstrumente, um die Qualität von Software in Zahlen auszudrücken (quantitativ zu machen).

Software hat Eigenschaften, die schwer greifbar sind (z. B. „Wartbarkeit“, „Transparenz“ oder „Effizienz“). Metriken machen diese „unsichtbaren“ Aspekte sichtbar und objektiv vergleichbar. Das Management und die Entwickler sollen nicht nach Bauchgefühl entscheiden, sondern auf Basis von Daten steuern können.

Nicht jede Zahl ist sinnvoll. Damit eine Messung verlässlich ist, muss sie laut Hoffmann (2013) folgende Kriterien erfüllen:

  • Objektivität: Das Ergebnis darf nicht von der Person abhängen, die misst. (Computergestützte Messungen sind hier am besten).
  • Robustheit: Wenn man zweimal das Gleiche misst, muss zweimal die gleiche Zahl herauskommen (Wiederholbarkeit).
  • Vergleichbarkeit: Man muss die Werte verschiedener Projekte miteinander vergleichen können.
  • Ökonomie: Der Aufwand, die Daten zu erheben, darf nicht teurer sein als der Nutzen, den man daraus zieht.
  • Korrelation: Die gemessene Zahl muss auch wirklich einen Rückschluss auf das zulassen, was man eigentlich wissen will (z. B. Hohe Zahl = Schlechte Wartbarkeit).
  • Verwertbarkeit: Die Zahl darf nicht nur "da" sein, sie muss helfen, Entscheidungen für die Zukunft zu treffen.

Metriken der imperativen Programmierung

LOC & NCSS

Diese Metriken sind am einfachsten zu verstehen, aber auch am ungenausten, da sie stark vom Schreibstil des Programmierers abhängen.

  • LOC (Lines of Code): Man zählt einfach jede Zeile in der Datei. Leerzeilen und Kommentare blähen allerdings den Wert auf, ohne dass das Programm komplexer ist.

  • NCSS (Non Commented Source Statements): Man zählt nur die echten Befehlszeilen. Kommentare, Leerzeilen und Header werden ignoriert. Wenn man NCSS durch LOC teilt, sieht man, wie gut der Code dokumentiert ist (der „Dokumentationsgrad“).

Die Halstead-Metriken (Lexikalische Analyse)

Hier wird der Text der Software wie eine Sprache analysiert. Halstead schaut nicht auf die Zeilen, sondern auf die „Wörter“ (Tokens). Man zählt Operatoren (Verben/Aktionen wie +, -, =, if) und Operanden (Substantive/Daten wie x, 5, result). Aus der Anzahl dieser Elemente berechnet Halstead über Formeln verschiedene Werte:

  • Volumen (V): Wie viele Entscheidungen mussten beim Schreiben getroffen werden?
  • Difficulty (D): Wie schwer ist der Code zu verstehen?
  • Effort (E): Wie hoch war der Aufwand?

Diese Metrik ist gut, um die Wartbarkeit einzuschätzen, aber manchmal ungenau, da die logische Struktur ignoriert wird.

Die McCabe-Metrik (Zyklomatische Komplexität)

Dies ist eine der wichtigsten Metriken für die Testplanung. Sie misst die logische Komplexität des Programmflusses (Kontrollfluss). Man stellt das Programm als Graphen dar (Knoten sind Befehle, Kanten sind die Verbindungen).

Die Formel: M=E−N+2 mit

  • E = Anzahl der Kanten/Verbindungen und
  • N = Anzahl der Knoten/Befehle

Die Zahl M sagt aus, wie viele verschiedene Wege es durch dein Programm gibt (durch if, while, etc.) wobei

  • Je höher die Zahl, desto schlechter die Wartbarkeit.
  • Je höher die Zahl, desto mehr Testfälle brauchst du (um jeden Pfad einmal abzulaufen).

Als Faustregel gilt, dass ein Wert über 10 als kritisch gilt und vermieden werden sollte (z.B. durch Refactoring).

Metriken der objektorientierten Programmierung

Metriken (wie LOC oder McCabe) eignen sich nicht für modernes (OOP) Software-Engineering: Sie verstehen keine Klassen, keine Vererbung und keine Objekte.

Deshalb wurden objektorientierte Metriken entwickelt. Diese schauen nicht nur auf den Code in einer Funktion, sondern darauf, wie Klassen miteinander verwandt sind und interagieren.

Man kann die Metriken in drei Kategorien aufteilen:

  1. Vererbungsmetriken (Die Hierarchie) Diese Metriken messen den "Stammbaum" deiner Klassen.
  • DIT (Depth of Inheritance Tree): Wie tief steckt eine Klasse in der Vererbungshierarchie? Es wird der Abstand von der Klasse bis ganz oben zur Wurzel-Klasse gemessen. Je tiefer, desto komplexer und schwerer zu verstehen. Der Wert sollte nicht größer als 6 oder 7 sein.

  • NOC (Number of Children): Wie viele direkte Unterklassen hat eine Klasse? Eine Klasse mit vielen "Kindern" (hoher NOC) ist sehr einflussreich. Wenn hier ein Fehler entsteht, erben ihn alle Kinder. Sie muss extrem gut getestet werden.

  1. Kohäsionsmetrik (Der innere Zusammenhalt)

Hier geht es darum, ob eine Klasse logisch zusammengehört oder ob sie Dinge tut, die nichts miteinander zu tun haben.

  • LCOM (Lack of Cohesion in Methods): Wörtlich: „Mangel an Zusammenhalt“. Man prüft, ob die Methoden einer Klasse auf die gleichen Attribute zugreifen.Es soll idealtypisch ein niedriger LCOM-Wert erreicht werden. Ein hoher Lack of Cohesion ist schlecht. Ein niedriger Wert (gegen 0) bedeutet, dass alle Methoden gut zusammenarbeiten (hohe Kohäsion).
  1. Struktur- und Kopplungsmetriken (Die Vernetzung). Diese Metriken messen, wie stark Klassen voneinander abhängig sind (Kopplung).
  • Fan-In (Fin​): Wie viele andere Klassen nutzen mich? Ein niedriger Fan-In deutet oft auf Klassen am "oberen Ende" der Architektur hin (Steuerungseinheiten, die niemanden bedienen müssen, sondern selbst agieren).
  • Fan-Out (Fout​): Wie viele andere Klassen nutze ich? Ein hoher Fan-Out bedeutet, dass eine Klasse viele andere steuert oder delegiert.
  • CBO (Coupling Between Objects): Ein allgemeines Maß dafür, mit wie vielen anderen Klassen eine Klasse verbunden ist. Das Ziel ist es, die Abhängigkeiten zu minimieren (leichtere Wartung).

(1.4) Divide et Impera - Divide and Conquer

„Divide et Impera“ (Teile und Herrsche) ist eine Methode, um große, komplexe Softwareprojekte erst beherrschbar zu machen.

Die Grundidee hinter diesem Prinzip liegt es darin, die Komplexität durch Zerlegung zu reduzieren. Ein komplettes Softwaresystem ist zu groß, um es als Ganzes zu verstehen oder zu bauen. Deshalb zerlegt man das „große Problem“ in viele kleine, voneinander unabhängige „Teilprobleme“. Wenn man die Teillösungen zusammensetzt, hat man das Gesamtsystem. Das funktioniert nur, wenn die Schnittstellen (wie wir sie vorher besprochen haben) sauber definiert sind.

Dekomposition

Wie wird ein System zerlegt? Es gibt unterschiedliche Ansätze:

  • Funktionale Dekomposition (Nach Aufgaben): Man zerlegt das System anhand dessen, was es tut. Beispiel: „Kundenverwaltung“ wird zerlegt in „Anlegen“, „Bearbeiten“, „Löschen“.
  • Strukturelle Dekomposition (Nach Bausteinen): Man zerlegt das System in technische Ebenen oder Komponenten. Beispiel: „Webshop“ wird zerlegt in „Benutzeroberfläche“ (UI), „Logik-Server“, „Datenbank“.
Parametrisierung

Zusätzlich zur Zerlegung sollen Module konfigurierbar (parametrisierbar) sein. Anstatt ein Modul starr für einen Zweck zu bauen, baut man es so, dass es durch Einstellungen (Parameter) an verschiedene Situationen angepasst werden kann. Das erhöht die Wiederverwendung. Dadurch soll eine bessere Austauschbarkeit und höhere Flexibilität der Modulen erreicht werden. Teams können gleichzeitig an verschiedenen Modulen arbeiten (Arbeitsteilung = Parallelität). Wenn sich eine Anforderung ändert (z. B. „Neue Steuerberechnung“), muss man nur ein Modul anfassen, nicht das ganze System (Warbarkeit). Wenn die Schnittstelle stimmt, kann man ein altes Modul einfach gegen ein besseres austauschen (wie einen alten Motor in einem Auto), ohne dass der Rest des Autos merkt, dass etwas passiert ist (Austauschbarkeit).

(1.5) Design by Contract

Das Prinzip Design by Contract (vertragsbasierte Programmierung) lässt sich durch möglichst frühzeitige Spezifikation von Schnittstellen umsetzen, die über den Systemlebenszyklus hinweg möglichst stabil gehalten werden. Dies erleichtert zudem die Abstraktion der konkreten Implementierung. Es erweist sich als hilfreich, Schnittstellen so zu gestalten, dass in sich geschlossene Teilaufgaben gelöst werden, und diese vollständig und detailliert zu beschreiben.

Die Herausforderung besteht darin, dennoch allgemein zu bleiben. Dazu muss ggf. eine speziellere Aufgabenstellung, die etwa im Rahmen der Dekomposition der Aufgabenstellung anfällt, so verallgemeinert werden, dass die speziellere Aufgabe durch entsprechende Parameterauswahl gelöst wird. Ziel dieses Vorgehens ist der Erhalt sogenannter „schmaler“ Schnittstellen, also von Schnittstellen mit einer überschaubaren Parameteranzahl. Zudem kann die entstehende Komponente desto besser für andere Aufgabenstellungen genutzt werden, je allgemeiner sie ausgelegt ist. Fazit: „Die Schnittstellenbeschreibung bildet einen Vertrag zwischen den Nutzern und den Implementierern einer Schnittstelle. Der Nutzer verlässt sich auf das in der Schnittstelle spezifizierte Verhalten, der Implementierer garantiert die Schnittstelle“ (Broy/Kuhrmann 2021, S.341).